﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Graphics.Implementation;
using Tesla.Math;
using Tesla.Util;
using D3D = SlimDX.Direct3D10;
using DXGI = SlimDX.DXGI;
using SDX = SlimDX;

namespace Tesla.Direct3D10.Graphics.Implementation {
    /// <summary>
    /// Concrete Direct3D10 implementation of <see cref="Texture2D"/> and <see cref="RenderTarget2D"/>.
    /// </summary>
    public sealed class D3D10Texture2DImplementation : Texture2DImplementation {
        private D3D10Renderer _renderer;
        private D3D.Device _graphicsDevice;
        private D3D.ShaderResourceView _shaderResourceView;
        private D3D.RenderTargetView _renderTargetView;
        private D3D.DepthStencilView _depthStencilView;
        private D3D.Texture2D _texture2D;
        private D3D.Texture2D _staging;
        private D3D.ResourceUsage _usage;
        private int _mipCount;

        /// <summary>
        /// Gets the D3D10Renderer that created this implementation.
        /// </summary>
        internal D3D10Renderer Renderer {
            get {
                return _renderer;
            }
        }

        /// <summary>
        /// Gets the D3D10 ShaderResourceView.
        /// </summary>
        internal D3D.ShaderResourceView D3D10ShaderResourceView {
            get {
                return _shaderResourceView;
            }
        }

        /// <summary>
        /// Gets the D3D10 RenderTargetView.
        /// </summary>
        internal D3D.RenderTargetView D3D10RenderTargetVew {
            get {
                return _renderTargetView;
            }
        }

        /// <summary>
        /// Gets the D3D10 DepthStencilView.
        /// </summary>
        internal D3D.DepthStencilView D3D10DepthStencilView {
            get {
                return _depthStencilView;
            }
        }

        /// <summary>
        /// Gets the D3D10 Texture2D.
        /// </summary>
        internal D3D.Texture2D D3D10Texture2D {
            get {
                return _texture2D;
            }
        }

        /// <summary>
        /// Gets the number of mip map levels in the texture resource.
        /// </summary>
        public override int MipCount {
            get {
                return _mipCount;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10Texture2DImplementation"/>, for Texture2D.
        /// </summary>
        /// <param name="renderer">The D3D10 renderer.</param>
        /// <param name="width">The texture width in pixels.</param>
        /// <param name="height">The texture height in pixels.</param>
        /// <param name="genMipMaps">True if a mipmap chain should be generated or not.</param>
        /// <param name="format">The surface format.</param>
        /// <param name="data">The data to initialize the first mip map level.</param>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if there was an error writing to the texture.</exception>
        internal D3D10Texture2DImplementation(D3D10Renderer renderer, int width, int height, bool genMipMaps, SurfaceFormat format, DataBuffer data)
            : base(width, height, format) {
            //Set common properties
            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            //Do we want to generate mip maps, and do we have the data to do so? (if not, cancel mip map generation)
            bool canGenMipmaps = (genMipMaps && (data != null));

            //Setup texture description
            D3D.Texture2DDescription descTex = new D3D.Texture2DDescription();
            descTex.ArraySize = 1;
            descTex.Width = width;
            descTex.Height = height;
            descTex.Usage = _usage = D3D.ResourceUsage.Default;
            descTex.CpuAccessFlags = D3D.CpuAccessFlags.None;
            descTex.Format = D3D10Helper.ToD3DSurfaceFormat(format);
            descTex.SampleDescription = new DXGI.SampleDescription(1, 0); //Default sample desc
            descTex.MipLevels = (genMipMaps) ? 0 : 1;

            //Set mip map generation params
            if(canGenMipmaps) {
                descTex.BindFlags = D3D.BindFlags.ShaderResource | D3D.BindFlags.RenderTarget;
                descTex.OptionFlags = D3D.ResourceOptionFlags.GenerateMipMaps;
            } else {
                descTex.BindFlags = D3D.BindFlags.ShaderResource;
                descTex.OptionFlags = D3D.ResourceOptionFlags.None;
            }

            //Create the texture and shader view
            _texture2D = new D3D.Texture2D(_graphicsDevice, descTex);
            _shaderResourceView = new D3D.ShaderResourceView(_graphicsDevice, _texture2D);

            //Add to tracker
            _renderer.Resources.AddTrackedObject(_texture2D.ComPointer, this);
            _renderer.Resources.AddTrackedObject(_shaderResourceView.ComPointer, this);

            //Set the final mip count
            _mipCount = _texture2D.Description.MipLevels;

            //Now set the initial data if its present
            if(data != null) {
                try {
                    this.SetData<byte>(data.ByteDataCopy, 0, null, 0, data.ElementSizeInBytes * data.Length);

                    if(genMipMaps) {
                        _graphicsDevice.GenerateMips(_shaderResourceView);
                    }

                    //Dispose of the staging texture
                    if(_staging != null) {
                        _staging.Dispose();
                        _staging = null;
                    }
                } catch(Exception e) {
                    Dispose();
                    throw new TeslaException("Error setting Texture data.", e);
                }
            }
        }


        /// <summary>
        /// Creates a new instance of <see cref="D3D10Texture2DImplementation"/>, for RenderTarget2D.
        /// </summary>
        /// <param name="renderer">The D3D10 renderer.</param>
        /// <param name="width">The texture width in pixels.</param>
        /// <param name="height">The texture height in pixels.</param>
        /// <param name="genMipMaps">True if a mipmap chain should be generated or not.</param>
        /// <param name="format">The surface format.</param>
        /// <param name="depthFormat">The depth-stencil format.</param>
        /// <param name="multiSampleCount">The multisample count.</param>
        /// <param name="usage">The target usage.</param>
        /// <exception cref="System.ArgumentException">Thrown if the formats are invalid to be used in a render target.</exception>
        internal D3D10Texture2DImplementation(D3D10Renderer renderer, int width, int height, bool genMipMaps, SurfaceFormat format, DepthFormat depthFormat, int multiSampleCount, RenderTargetUsage usage) 
            : base(width, height, format, depthFormat, multiSampleCount, usage) {

            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;

            if(!renderer.Adapter.QueryRenderTargetFormat(format, depthFormat, multiSampleCount)) {
                throw new ArgumentException("Format combination not supported.");
            }

            D3D.Texture2DDescription desc2D = new D3D.Texture2DDescription();
            desc2D.ArraySize = 1;
            desc2D.BindFlags = D3D.BindFlags.ShaderResource | D3D.BindFlags.RenderTarget;
            desc2D.CpuAccessFlags = D3D.CpuAccessFlags.None;
            desc2D.Format = D3D10Helper.ToD3DSurfaceFormat(format);
            desc2D.Height = height;
            desc2D.Width = width;
            desc2D.Usage = _usage = D3D.ResourceUsage.Default;
            if(genMipMaps) {
                desc2D.OptionFlags = D3D.ResourceOptionFlags.GenerateMipMaps;
            } else {
                desc2D.OptionFlags = D3D.ResourceOptionFlags.None;
            }
            desc2D.MipLevels = (genMipMaps) ? 0 : 1;
            if(multiSampleCount > 1) {
                desc2D.SampleDescription = new SlimDX.DXGI.SampleDescription(multiSampleCount, 0);
            } else {
                desc2D.SampleDescription = new DXGI.SampleDescription(1, 0);
            }

            _texture2D = new D3D.Texture2D(_graphicsDevice, desc2D);
            _shaderResourceView = new D3D.ShaderResourceView(_graphicsDevice, _texture2D);
            _renderTargetView = new D3D.RenderTargetView(_graphicsDevice, _texture2D);
            _graphicsDevice.ClearRenderTargetView(_renderTargetView, new SDX.Color4(1.0f, 0, 0, 0));

            //Add to tracker
            _renderer.Resources.AddTrackedObject(_texture2D.ComPointer, this);
            _renderer.Resources.AddTrackedObject(_shaderResourceView.ComPointer, this);
            _renderer.Resources.AddTrackedObject(_renderTargetView.ComPointer, this);

            _mipCount = _texture2D.Description.MipLevels;

            if(depthFormat != DepthFormat.None) {
                D3D.Texture2DDescription dbdesc = new D3D.Texture2DDescription();
                dbdesc.ArraySize = 1;
                dbdesc.BindFlags = D3D.BindFlags.DepthStencil;
                dbdesc.CpuAccessFlags = D3D.CpuAccessFlags.None;
                dbdesc.Format = D3D10Helper.ToD3DDepthFormat(depthFormat);
                dbdesc.Height = height;
                dbdesc.Width = width;
                dbdesc.Usage = D3D.ResourceUsage.Default;
                dbdesc.OptionFlags = D3D.ResourceOptionFlags.None;
                dbdesc.MipLevels = 1;
                if(multiSampleCount > 1) {
                    dbdesc.SampleDescription = new SlimDX.DXGI.SampleDescription(multiSampleCount, 0);
                } else {
                    dbdesc.SampleDescription = new SlimDX.DXGI.SampleDescription(1, 0);
                }

                using(D3D.Texture2D depthBuffer = new D3D.Texture2D(_graphicsDevice, dbdesc)) {
                    _depthStencilView = new D3D.DepthStencilView(_graphicsDevice, depthBuffer);
                    if(depthFormat == Tesla.Graphics.DepthFormat.Depth24Stencil8) {
                        _graphicsDevice.ClearDepthStencilView(_depthStencilView, D3D.DepthStencilClearFlags.Depth | D3D.DepthStencilClearFlags.Stencil, 1.0f, 0);
                    } else {
                        _graphicsDevice.ClearDepthStencilView(_depthStencilView, D3D.DepthStencilClearFlags.Depth, 1.0f, 0);
                    }

                    //Add to tracker
                    _renderer.Resources.AddTrackedObject(_depthStencilView.ComPointer, this);
                }
            }
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="D3D10Texture2DImplementation"/> is reclaimed by garbage collection.
        /// </summary>
        ~D3D10Texture2DImplementation() {
            Dispose(false);
        }

        /// <summary>
        /// Gets the data from the texture.
        /// </summary>
        /// <typeparam name="T">Type of data in the array.</typeparam>
        /// <param name="data">Array of data.</param>
        /// <param name="mipLevel">Mip map level to access.</param>
        /// <param name="subimage">Rectangle representing a sub-image of the 2D texture to read from, if null the whole image is read from.</param>
        /// <param name="startIndex">Starting index in the array to start writing to.</param>
        /// <param name="elementCount">Number of elements to read.</param>
        /// <exception cref="System.ArgumentException">Thrown if the format byte size of the type to read and the texture do not match, the subimage
        /// dimensions are invalid, or if the total size to read and the total size in the texture do not match</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if there was an error reading from the texture.</exception>
        public override void GetData<T>(T[] data, int mipLevel, Rectangle? subimage, int startIndex, int elementCount) {
            if(_texture2D == null || _texture2D.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            if(mipLevel < 0 || mipLevel >= _mipCount) {
                throw new ArgumentOutOfRangeException("mipLevel", String.Format("Mip level is out of range. Must be between 0 and {0}.", _mipCount.ToString()));
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            int formatSize = D3D10Helper.FormatSize(base.Format);
            int elemSize = MemoryHelper.SizeOf<T>();
            CheckSizes(formatSize, elemSize);

            //Calc subresource dimensions 
            int width = (int) MathHelper.Max(1, base.Width >> mipLevel);
            int height = (int) MathHelper.Max(1, base.Height >> mipLevel);

            //Get the dimensions, if its null we copy the whole texture
            Rectangle rect;
            if(subimage.HasValue) {
                rect = subimage.Value;
                CheckRectangle(rect, ref width, ref height);
            } else {
                rect = new Rectangle(0, 0, width, height);
            }

            CheckTotalSize(base.Format, ref formatSize, ref width, ref height, elemSize, elementCount);

            //Create staging
            CreateStaging();

            try {
                int row = rect.Y;
                int col = rect.X;

                //Do resource copy
                if(base.Format == SurfaceFormat.DXT1 || base.Format == SurfaceFormat.DXT3 || base.Format == SurfaceFormat.DXT5) {
                    _graphicsDevice.CopyResource(_texture2D, _staging);
                } else {
                    D3D.ResourceRegion region = new D3D.ResourceRegion();
                    region.Left = col;
                    region.Right = col + width;
                    region.Top = row;
                    region.Bottom = row + height;
                    region.Front = 0;
                    region.Back = 1;
                    _graphicsDevice.CopySubresourceRegion(_texture2D, mipLevel, region, _staging, mipLevel, col, row, 0);
                }

                SDX.DataRectangle dataRect = _staging.Map(mipLevel, D3D.MapMode.Read, D3D.MapFlags.None);
                SDX.DataStream ds = dataRect.Data;
                int pitch = dataRect.Pitch;

                int index = startIndex;
                int count = elementCount;

                //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));
                //Go row by row
                for(int i = row; i < height; i++) {
                    //Set the position
                    ds.Position = (i * pitch) + (col * formatSize);
                    //Break if we've run out of elements or on the very last row that isn't complete
                    if(count <= 0) {
                        break;
                    } else if(count < actWidth) {
                        ds.ReadRange<T>(data, index, count);
                        break;
                    }
                    //Otherwise, copy the whole row and increment/decrement the index and count
                    ds.ReadRange<T>(data, index, actWidth);
                    index += actWidth;
                    count -= actWidth;
                }
                _staging.Unmap(mipLevel);
            } catch(Exception e) {
                throw new TeslaException("Error reading from D3D10 Texture2D.", e);
            }
        }

        /// <summary>
        /// Sets the data to the texture.
        /// </summary>
        /// <typeparam name="T">Type of data in the array.</typeparam>
        /// <param name="data">Array of data.</param>
        /// <param name="mipLevel">Mip map level to access.</param>
        /// <param name="subimage">Rectangle representing a sub-image of the 2D texture to write to, if null the whole image is written to.</param>
        /// <param name="startIndex">Starting index in the array to start reading from.</param>
        /// <param name="elementCount">Number of elements to write.</param>
        /// <exception cref="System.ArgumentException">Thrown if the format byte size of the type to write and the texture do not match, the subimage
        /// dimensions are invalid, or if the total size to write and the total size in the texture do not match</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if there was an error writing to the texture.</exception>
        public override void SetData<T>(T[] data, int mipLevel, Math.Rectangle? subimage, int startIndex, int elementCount) {
            if(_texture2D == null ||_texture2D.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            if(mipLevel < 0 || mipLevel >= _mipCount) {
                throw new ArgumentOutOfRangeException("mipLevel", String.Format("Mip level is out of range. Must be between 0 and {0}.", _mipCount.ToString()));
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            int formatSize = D3D10Helper.FormatSize(base.Format);
            SurfaceFormat format = base.Format;
            int elemSize = MemoryHelper.SizeOf<T>();
            CheckSizes(formatSize, elemSize);

            //Calc subresource dimensions 
            int width = (int) MathHelper.Max(1, base.Width >> mipLevel);
            int height = (int) MathHelper.Max(1, base.Height >> mipLevel);

            //Get the dimensions, if its null we copy the whole texture
            Rectangle rect;
            if(subimage.HasValue) {
                rect = subimage.Value;
                CheckRectangle(rect, ref width, ref height);
            } else {
                rect = new Rectangle(0, 0, width, height);
            }

            CheckTotalSize(base.Format, ref formatSize, ref width, ref height, elemSize, elementCount);

            //Create staging for non-dynamic textures
            if(_usage == D3D.ResourceUsage.Default) {
                CreateStaging();
            }

            try {
                if(_usage == D3D.ResourceUsage.Default) {
                    SDX.DataRectangle dataRect = _staging.Map(mipLevel, D3D.MapMode.Write, D3D.MapFlags.None);
                    SDX.DataStream ds = dataRect.Data;
                    int row = rect.Y;
                    int col = rect.X;
                    int pitch = dataRect.Pitch;

                    int index = startIndex;
                    int count = elementCount;
                    //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                    int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));
                    //Go row by row
                    for(int i = row; i < height; i++) {
                        //Set the position
                        ds.Position = (i * pitch) + (col * formatSize);
                        //Break if we've run out of elements or on the very last row that isn't complete
                        if(count <= 0) {
                            break;
                        } else if(count < actWidth) {
                            ds.WriteRange<T>(data, index, count);
                            break;
                        }
                        //Otherwise, copy the whole row and increment/decrement the index and count
                        ds.WriteRange<T>(data, index, actWidth);
                        index += actWidth;
                        count -= actWidth;
                    }
                    _staging.Unmap(mipLevel);

                    //Do resource copy
                    if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
                        _graphicsDevice.CopyResource(_staging, _texture2D);
                    } else {
                        D3D.ResourceRegion region = new D3D.ResourceRegion();
                        region.Left = col;
                        region.Right = col + width;
                        region.Top = row;
                        region.Bottom = row + height;
                        region.Front = 0;
                        region.Back = 1;
                        _graphicsDevice.CopySubresourceRegion(_staging, mipLevel, region, _texture2D, mipLevel, col, row, 0);
                    }
                } else {
                    SDX.DataRectangle dataRect = _texture2D.Map(mipLevel, D3D.MapMode.WriteDiscard, D3D.MapFlags.None);
                    SDX.DataStream ds = dataRect.Data;
                    int row = rect.Y;
                    int col = rect.X;
                    int pitch = dataRect.Pitch;

                    int index = startIndex;
                    int count = elementCount;
                    //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                    int actWidth = (int) MathHelper.Floor(width * (formatSize / elemSize));
                    //Go row by row
                    for(int i = row; i < height; i++) {
                        //Set the position
                        ds.Position = (i * pitch) + (col * formatSize);
                        //Break if we've run out of elements or on the very last row that isn't complete
                        if(count <= 0) {
                            break;
                        } else if(count < actWidth) {
                            ds.WriteRange<T>(data, index, count);
                            break;
                        }
                        //Otherwise, copy the whole row and increment/decrement the index and count
                        ds.WriteRange<T>(data, index, actWidth);
                        index += actWidth;
                        count -= actWidth;
                    }
                    _texture2D.Unmap(mipLevel);
                }
            } catch(Exception e) {
                throw new TeslaException("Error writing to D3D10 Texture2D.", e);
            }
        }

        private static void CheckSizes(int formatSize, int elementSize) {
            if(formatSize != elementSize) {
                if((formatSize <= elementSize) || ((formatSize % elementSize) != 0)) {
                    throw new ArgumentException("Element format size does not match texture's expected format size.");
                }
            }
        }

        private static void CheckRectangle(Rectangle rect, ref int width, ref int height) {
            int x = rect.X;
            int y = rect.Y;
            int rectWidth = rect.Width;
            int rectHeight = rect.Height;
            if(x < 0 || y < 0 || rectWidth <= 0 || rectHeight <= 0) {
                throw new ArgumentException("Invalid rectangle, x,y cannot be negative or width and height must be more than zero.");
            }

            if(rect.Right > width && rect.Bottom > height) {
                throw new ArgumentException("Invalid rectangle subimage.");
            }

            width = rectWidth;
            height = rectHeight;
        }

        private static void CheckTotalSize(SurfaceFormat format, ref int formatSize, ref int width, ref int height, int elementSize, int elementCount) {
            if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
                width = (width + 3) >> 2;
                height = (height + 3) >> 2;
                formatSize = (format == SurfaceFormat.DXT1) ? 8 : 16;
            }

            int totalSize = width * height * formatSize;
            if(elementSize * elementCount != totalSize) {
                throw new ArgumentException("Total size of data to write does not match expected total size.");
            }
        }

        private void CreateStaging() {
            if(_staging == null) {

                D3D.Texture2DDescription oDesc = _texture2D.Description;

                D3D.Texture2DDescription desc = new D3D.Texture2DDescription();
                desc.ArraySize = 1;
                desc.BindFlags = D3D.BindFlags.None;
                desc.CpuAccessFlags = D3D.CpuAccessFlags.Write | D3D.CpuAccessFlags.Read;
                desc.Format = D3D10Helper.ToD3DSurfaceFormat(base.Format);
                desc.Height = base.Height;
                desc.Width = base.Width;
                desc.MipLevels = _mipCount;
                desc.SampleDescription = new DXGI.SampleDescription(1, 0);
                desc.Usage = D3D.ResourceUsage.Staging;

                _staging = new D3D.Texture2D(_graphicsDevice, desc);

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_staging.ComPointer, this);
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing) {
            if(!IsDisposed) {
                //Dispose of managed resources
                if(disposing) {
                    if(_shaderResourceView != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_shaderResourceView.ComPointer);
                        }
                        _shaderResourceView.Dispose();
                        _shaderResourceView = null;
                    }

                    if(_renderTargetView != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_renderTargetView.ComPointer);
                        }
                        _renderTargetView.Dispose();
                        _renderTargetView = null;
                    }

                    if(_depthStencilView != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_depthStencilView.ComPointer);
                        }
                        _depthStencilView.Dispose();
                        _depthStencilView = null;
                    }

                    if(_texture2D != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_texture2D.ComPointer);
                        }
                        _texture2D.Dispose();
                        _texture2D = null;
                    }

                    if(_staging != null) {
                        if(_renderer != null) {
                            _renderer.Resources.RemoveTrackedObject(_staging.ComPointer);
                        }
                        _staging.Dispose();
                        _staging = null;
                    }
                }
                base.Dispose(disposing);
            }
        }
    }
}
